
8700 CPU

Introduction

The 8700 CPU architecture is designed to strike a balance between minimalism in implementation, simplicity of use, and retro appeal.
It's basically what I wish the 6502 was. If you have experience with 6502 assembly, this should all seem very familiar.

The 8700 is an 8-bit CPU with 16-bit addressing.
It has an 8-bit bidirectional data bus, accessing the same address space for both code and data, and a 16-bit address bus.

I/O:
	Name        Bits  Direction  Description
	Address     16    Out        Address bus, working at one access (read or write) per clock cycle.
	Data        8     In/Out     Bidirectional data bus - Output in write mode, Input in read mode.
	Read/Write  1     Out        High means write data bus to memory at specified address, low means read.
	User Mode   1     Out        High means execution is in user mode, low means interrupt / supervisor mode.
	Interrupt   1     In         High input triggers an interrupt if execution is not already in interrupt mode.

Registers include an accumulator, two addressing registers, a stack pointer, and a flags register;
as well as a program counter and interrupt vector register.

Registers:
	Name  Bits  Description
	a      8    Accumulator
	x      8    Index 1 / Address high byte
	y      8    Index 2 / Address low byte
	f      8    Flags register (User Run 0 0 0 0 Zero Carry)
	s     16    Stack pointer
	pc    16    Program counter
	ic    16    Interrupt vector


Basic Instructions

The 8700 CPU has a set of basic arithmetic and logic instructions, including adding and subtracting from any register,
and boolean logic and bitshift operations on the accumulator.

Load and store instructions: cla (Clear A), clx, cly, clf, lda, ldx, ldy, ldf, sta, stx, sty, stf
Arithmetic and logic instructions on the accumulator: add, sub, and, ior (Inclusive OR), xor, cmp
Arithmetic instructions on other registers: adx, sbx, ady, sby, ads, sbs
No-operand arithmetic instructions: inc, dec, inx, dex, iny, dey, ins, des, tst
No-operand logic instructions: shr, shl, ror, rol, sra, swn (Swap nibbles)

All instructions that take an operand can use one of eight addressing modes:
	Name            Usage      Cycles  Description
	A               adx a      1       Operand is the A register
	X               lda x      1       Operand is the X register
	Y               ldx y      1       Operand is the Y register
	Immediate       sub #$10   2       Operand is an 8-bit immediate encoded after the instruction byte
	Indirect        lda xy     2       Operand is the data in address X,Y - X is the high byte, Y is the low byte
	Stack-Relative  and $18    3       Operand is the data in address SP+I, where I is an 8-bit signed offset from the stack pointer
	Absolute        ldy $6800  4       Operand is the data at the given address. Address is $HHLL.
	Stack           pha        2       If the operation is a read, pops from the stack. If the operation is a write, pushes to the stack.

All load, arithmetic, and logic instructions set the Carry and Zero flags based on the result.


Program Control

The 8700 has six conditional relative branch instructions:
	jpr - JumP Relative (always)
	jpz - JumP relative if Zero flag is set
	jnz - Jump if Zero flag is Not set
	jpc - JumP if Carry flag is set
	jnc - Jump if Carry flag is Not set
Relative jump instructions take a signed 8-bit offset, which is added to the PC if the condition is true.
Offsets are relative to the address of the instruction after the jump.

There are three long jump instructions:
	jmp, i.e. jmp $1000 - JuMP - Set PC to the address given by the addressing mode.
	jsr, i.e. jsr xy - Jump into SubRoutine - Push the PC of the next instruction onto the stack, high byte first, and then jump.
	rts - ReTurn from Subroutine - Pop PC off the stack, high byte first.


The Stack

The 8700's stack can be located anywhere in memory.
The stack grows upward, with the pointer pointing to the next available space above the current data.
The stack is used extensively in programming, as it provides space for return addresses, function arguments, and even local variables.
The stack-relative addressing mode is used much like the 6502's zero page;
but it has the benefits of being unlimited in size, mobile in memory, and unique per-function if you use stack frames.

Stack instructions:
	ph(a/x/y/f), i.e. pha - Push a register onto the stack, post-incrementing SP.
	pp(a/x/y/f), i.e. ppf - Pop a register from the stack, pre-decrementing SP.
	ads, i.e. ads #$20 - ADd a value to the Stack pointer
	sbs, i.e. sbs $00 - SuBtract a value from the Stack pointer
	xxs - eXchange X and y with Stack pointer - Swap the values of X and Y with the Stack pointer register. X is exchanged with the high byte, Y with the low byte.

The stack-relative addressing mode can be used with any instruction taking an operand:
	stx $20 - Stores the value in X at (SP + 0x20)
	add $20 - Adds the value at (SP + 0x20) to the accumulator

The stack pointer register is initialized to 0 on startup. You may want to place the stack elsewhere in memory during initialization:
	lxy #stack_page   ; Load XY with the address of the stack
	xxs               ; Swap this address into the stack pointer

The stack can be located anywhere in memory:
.org $8000            ; Hypothetical location of RAM
stack_page:           ; Start the stack pointer here
	byte[$0100]       ; Allocate 256 bytes of space for the stack to grow upward


Interrupts

The 8700 has a minimalist but very powerful interrupt architecture.
There is only one interrupt, and a CPU register controls what code executes when it is triggered.
It is up to the interrupt service routine to decide what to do from there.
The CPU has a flag called the user mode flag (Highest bit of the flags register).
The user mode flag is is 1 when the CPU is running normally, and 0 when the CPU is servicing an interrupt.
The user mode flag is exposed on an output pin of the CPU. High indicates user mode, low indicates interrupt mode.

The CPU has two 16-bit program counter registers, the Program Counter (PC) and the Interrupt Vector (IV).
The Program Counter points to the next instruction to be executed.
The Interrupt Vector is swapped with the Program Counter when entering or exiting interrupt mode.

In interrupt mode, the xxi instruction is available, which swaps the X and Y registers with the IV register.
X is exchanged with the high byte of the interrupt vector, and Y the lower byte.
This allows interrupt code to see and control where execution occurs in user mode, but not the other way around.
Because of this privilege, interrupt mode is much like a supervisor mode, as it can control user-mode execution.
Thus the interrupt routine assumes the responsibility of something like a kernel.
By combining this with a memory architecture that utilizes the user mode output pin,
it is possible to create advanced privilege models, such as protected memory or even restricted peripheral access.

There are two instructions available to the programmer:
	rti - ReTurn from Interrupt - Swap PC and IV, set user flag
	brk - BReaK (software interrupt) - Swap PC and IV, clear user flag


Running and Halting

The 8700 has a flag called the run flag (Second highest bit of the flags register).
It determines whether the CPU will execute code in user mode, or pause awaiting an interrupt.
When the run flag is set, the CPU will execute user code normally.
When it is cleared, the CPU will only execute interrupt code (Only run when the interrupt flag is set).
If user mode is halted, it is up to the interrupt to execute the run instruction to allow it to run again.

Behavior truth table:
	Run User Action
	0   0    Run interrupt code
	0   1    Halt awaiting interrupt
	1   0    Run interrupt code
	1   1    Run user code

This behavior is controlled by the run and hlt instructions:
	hlt - Pause user code execution, wait for an interrupt to execute the run instruction
	run - Allow user code execution to resume when the interrupt exits


Getting Started

Upon powering up, the 8700 starts executing code from memory address 0, in interrupt mode.
The run flag is cleared on startup, so only interrupt code will run.
For simple programs, this starting memory may contain the program's code,
and the CPU may be halted when done by executing the irt instruction to enter user mode.

For more advanced programs, or those requiring interrupts, it is necessary to set up the interrupt vector,
set the run flag, and enter user mode.
This can be done with the following sequence of instructions:
	lxy #main_program      ; Load XY with the address of user code
	xxi                    ; Swap this address into the interrupt vector register
	run                    ; Unhalt execution in user mode
interrupt_routine:
	rti                    ; Enter user mode and start executing at main_program
	...
main_program:              ; Your code here

It is recommended that you set up the stack during initialization as well,
so any subsequent interrupts will have access to it (even those that occur immediately on the first rti).
See the section on the stack for details.

Upon exiting from interrupt mode, the interrupt vector will point to the next instruction after the rti.
In order to have an interrupt handler routine run repeatedly, you must make it loop. For example:
interrupt_routine:
	rti                    ; Return from interrupt after initialization, or looping back
	                       ; This is where interrupt execution begins - your code here
	jmp interrupt_routine  ; Jump back to the beginning and immediately return


Assembler

The included 8700 assembler is similar to other assemblers in syntax, but has some additional features for ease of programming.

Instructions are encoded according to predefined architecture-specific rules.
To understand how to write instructions, see the examples in this document, including the complete list of instructions with examples at the end.

Spaces of uninitialized data can be specified, i.e. to allocate RAM for a global variable. To do this, use the "byte" and "word" (2 bytes) keywords:
	global_var_1: word
	global_var_2: byte
This simply adjusts the memory mapping of other data and labels - so global_var_2 will be located 2 bytes after global_var_1.
Uninitialized space does not produce any output in the memory dump.

Adding a pair of brackets after a line repeats that line a given number of times:
	buffer: byte[$0100]     ; Allocate 256 bytes of buffer space
Multiple brackets can be used on one line:
	matrix: word[$20][$20]  ; Allocate a 32x32 matrix of words (2048 bytes)
This syntax can also be used on instructions:
	nop[$20]                ; Wait 32 cycles

To set the location of data in memory, use the .org (origin) directive:
	.org $FFFA
	fake_6502_nmi: word  ; Located at $FFFA
	fake_6502_res: word  ; Located at $FFFC
	fake_6502_irq: word  ; Located at $FFFE
Code does not have to be in order, so .org can be used to go back to a lower address, i.e.
	.org $8000 ; RAM
	...        ; Your variables here
	.org $1000 ; Program ROM
	...        ; Your code here
The assembler will detect and warn the programmer if a memory location is used in multiple places.

To align to a specific boundary, use the .aln (align) directive:
	.aln $10   ; Align to the next 16-byte boundary, leaving empty space if needed.
	.aln $0100 ; Align to the next page (256-byte) boundary.

Labels can be substituted for any 16-bit immediate, or 8-bit relative jump offset.
Labels are defined using the standard syntax of following the name with a colon:
print_char:
	jpz print_char_null
	sta mmio_display_char
print_char_null:
	rts
...
	jsr print_char
Label names must be unique - a name may only be used once in a program, even across different files.

Braces can be used instead of labels for short jumps.
A brace used as part of an instruction acts like a label reference to the matching brace.
A brace by itself acts like a label definition.
Brace labels appear after the instruction the brace is used in.
For example:
	jpz {
	brk
	}
Would act like:
	jpz no_brk
	brk
no_brk:
Braces can go forwards or backwards:
	{
		jsr print_char
		dcx
	jpx }
They can even be used for simultaneous forward and backward jumps:
	jpz {
	jsr print_char
	dec
	jnz }
Since brace labels appear after their corresponding line, this is the same as:
	jpz loop_end
loop_start:
	jsr print_char
	dec
	jnz loop_start
loop_end:

All whitespace is ignored - tabs and spaces may be used at any level, in any location.
Commas are treated as line breaks, to allow multiple instructions to occupy one line. For example:
	pha, phx, phy, ppf  ; Save all registers
Labels can also be on the same line as an instruction or data statement:
	sub_end: rts        ; Return
This also works with braces:
	iny, jpn {, inx, }   ; Increment X,Y with carry
Note the comma after inx. If it were not present, the closing brace would be considered an operand to the inx instruction.

A primitive preprocessor is included. To define a macro, use the .def directive:
	.def ONE #$01
All instances of "one" would then be replaced with #$01 in the rest of the code.
Label, macro names, and macro arguments may contain upper- or lower-case letters, numbers, and underscores, i.e. RAM_start or device_01
There are also function-like macros:
	.def JUMP_IF_LESS(val, label) cmp val, jpc label,
	...
	JUMP_IF_LESS(#$04, less_than_four)
Macro definitions must occupy one line.
Macro arguments can have percent signs on either side to concatenate them to other identifiers:
	.def INC_CARRY(r1, r2) in%r2, jnc { in%r1, },
	...
	INC_CARRY(x, y)

You can include other assembly code files in the compilation of a file, similar to C's #include, using the .inc directive:
	.inc "definitions.asm"
Files are searched for relative to the current working directory.

Strings can be inserted as data by enclosing them in double quotes:
	hello_world_str: "Hello, world!\n\0"
Standard escape sequences can be used. Strings are not automatically null-terminated, so the programmer must specify a "\0" at the end where needed.
Characters can also be used as immediate values:
	lda #'i'


Example program

The following is a simple "Hello, world" program, designed to demonstrate many of the assembler and processor features.
It assumes there is an MMIO register at $FFFF that displays any character written to it.
It prints the letter 'i' when an interrupt is triggered.

.def PRINT_A sta $FFFF
.def INC_CARRY(r1, r2) in%r2, jnc { in%r1, },

.org $0000                 ; Start of ROM

	lxy #main_program      ; Set up program entry point
	xxi
	
	lxy #stack_page        ; Set up stack
	xxs
	
	run                    ; Allow program to run on interrupt exit

interrupt_routine:
	rti                    ; Exit interrupt into main program
	
	pha                    ; Save accumulator on stack
	
	lda #'i'               ; Print 'i'
	PRINT_A
	
	ppa                    ; Restore accumulator
	jpr interrupt_routine  ; Loop back to beginning for next interrupt

main_program:              ; Entry point into user code
	
	lxy #hello_world       ; Call print_string function with the string pointer in XY
	jsr print_string
	
	hlt                    ; Stop user execution

print_string:              ; Function to print a null-terminated string, whose address is in XY
	lda xy                 ; Load next char
	jpz print_string_done  ; Exit if null
	PRINT_A                ; Print char
	INC_CARRY(x, y)
	jpr print_string       ; Continue to next char
print_string_done:
	rts                    ; Return

hello_world:
	"Hello, world!\n\0"    ; String to print, null-terminated

.org $8000                 ; Start of RAM
.aln $0100                 ; Allocate one page for the stack to grow upward
stack_page:
	byte[$0100]

Complete Instruction List

	nop (1): Do nothing for one cycle
	
	rti (1): ReTurn from Interrupt - Set user-mode flag, swap PC and IV
	brk (1): BReaK: (software interrupt) - Clear user-mode flag, swap PC and IV
	
	hlt (1): HaLT - Pause execution in user-mode. To resume, a run instruction must be executed in interrupt mode.
	run (1): RUN - Allow execution in user-mode.
	
	shr (1): logical SHift Right one bit, shifting in a 0 into the highest bit, and shifting the lowest bit into the carry flag. Set zero flag.
	shl (1): logical SHift Left one bit, shifting a 0 into the lowest bit, and shifting the highest bit into the carry flag. Set zero flag.
	ror (1): ROtate Right one bit through carry flag, set Z.
	rol (1): ROtate Left one bit through carry flag, set Z.
	sra (1): Arithmetic Shift Right one bit, copying the highest bit, and shifting the lowest bit into the carry flag. Set Z.
	swn (1): SWap Nibbles - Swap the lower and higher 4 bits of the accumulator, so aaaabbbb becomes bbbbaaaa. Set Z.
	
	xxs (1): eXchange X and y registers with the Stack pointer registers. X is the high byte, Y is the low byte.
	xxi (1): eXchange X and y registers with the Interrupt vector registers. X is the high byte, Y is the low byte. Can only be used in interrupt mode.
	
	jpr (2): JumP Relative - Add a signed 8-bit offset to the program counter, starting at the position after the jump offset byte.
	
	jpr - JumP Relative (always) - Add a signed 8-bit offset to the program counter, starting at the position after the jump offset byte.
	jpz - JumP relative if Zero flag is set. Takes 1 cycle if no jump is performed (Zero flag is not set), 2 if one is.
	jnz - Jump if Zero flag is Not set
	jpc - JumP if Carry flag is set
	jnc - Jump if Carry flag is Not set

	
	jmp $hhll (3): JuMP to the given 16-bit address
	jsr $hhll (5): Jump to SubRoutine at the given address. Pushes the next PC value onto the stack, high byte at the lower address, and jumps.
	jmp xy (1): JuMP to the address in the X and Y registers. X is the high byte, Y is the low byte.
	jsr xy (3): Jump to SubRoutine at the address in the X and Y registers, saving PC on the stack.
	rts (3): ReTurn from Subroutine - Pop 2 bytes off the stack, high byte at the lower address, and jump to that address.
	
	st(a/x/y/f) $hhll, i.e. stx $100F (4) - STore the given register at the given 16-bit address
	st(a/x/y/f) $bb, i.e. stf $30 (3) - STore the given register at the given offset from the stack pointer
	st(a/x/y/f) xy, i.e. sta xy (2) - STore the given register at the address in the X and Y registers. X is the high byte, Y is the low byte.
	
	cl(a/x/y/f), i.e. clx (1) - CLear (load 0 into) the given register. Set zero flag.
	ld(a/x/y/f) (a/x/y), i.e. lda x (1) - LoaD (copy) the second register into the first. The "ldf y" instruction is not available. F can not be loaded into another register directly - use the stack instead, i.e. phf, ppa.
	ld(a/x/y/f) #$bb, i.e. lda #$0A (2) - LoaD an 8-bit immediate value into the given register
	ld(a/x/y/f) $bb, i.e. ldx $30 (3) - LoaD the value at the given offset from the stack pointer into the given register
	ld(a/x/y/f) $hhll, i.e. ldy $8000 (4) - LoaD the value at the given address into the given register
	Note: Loading into the flags register only sets the carry and zero flags - The other flags are set through special instructions.
	Note: All loads set the zero flag according to the loaded value, except loads into F.
	
	lxy #$xxyy, i.e. lxy #$0102 - Load the X and Y registers simultaneously with a 16-bit immediate. X gets the high byte, Y the low byte.
	
	ph(a/x/y/f), i.e. phy (2) - PusH the given register onto the stack.
	pp(a/x/y/f), i.e. ppx (2) - PoP a value from the stack into the given register.
	
	Arithmetic instructions - These have the same addressing modes as the load instructions, so you can do "add #$20" or "xor $1001" the same way you can use "lda" for example.
	All arithmetic instructions take the same number of cycles as the load instruction of the same addressing mode.
	add, i.e. add #$20 - ADD a value to the a register, ignoring the carry flag, and setting the carry and zero flags.
	sub, i.e. sub xy - SUBtract a value from the a register, ignoring the carry flag, and setting the carry and zero flags.
	adx, i.e. adx $30 - ADd a value to the X register, setting C and Z.
	sbx, i.e. sbx $3030 - SuBtract a value from the X register, setting C and Z.
	ady - ADd a value to the Y register, set C and Z.
	sby - SuBtract a value from the Y register, set C and Z.
	ads - ADd a value to the Stack pointer register, set C and Z.
	sbs - SuBtract a value from the Stack pointer register, set C and Z.
	and - bitwise AND the a register with a value, set Z.
	ior - bitwise Inclusive OR the a register with a value, set Z.
	xor - bitwise eXclusive OR the a register with a value, set Z.
	cmp - CoMPare the accumulator to the operand, setting the zero and carry flags but not writing the result back to the accumulator.
	
	inc (1) - INCrement the a register, setting C and Z.
	dec (1) - DECrement the a register, setting C and Z.
	inx (1) - INcrement the X register, setting C and Z.
	dex (1) - DEcrement the X register, setting C and Z.
	iny (1) - INcrement the Y register, setting C and Z.
	dey (1) - DEcrement the Y register, setting C and Z.
	ins (1) - INcrement the Stack pointer register, setting C and Z.
	des (1) - DEcrement the Stack pointer register, setting C and Z.
	tst (1) - Compare accumulator with 0, setting the zero and carry flags but not saving the result.
